Lambda PowertoolsのParameters utilityで独自Providerを実装してGoogle Cloudからシークレット値を取得してみた

Lambda PowertoolsのParameters utilityで独自Providerを実装してGoogle Cloudからシークレット値を取得してみた

Clock Icon2024.09.06

リテールアプリ共創部@大阪の岩田です。

AWS Lambda Powertools for TypeScriptのParameters utilityは様々なパラメータストアからパラメータ値を取得するための高レベルなAPIを提供しています。デフォルトだとパラメータの取得元として以下のProviderが提供されており、それぞれ対応するパラメータストアから簡単にパラメータ値が取得できるようになっています。

  • SSMProvider
  • SecretsProvider
  • AppConfigProvider
  • DynamoDBProvider

また、これらのProviderに加えて独自のProviderを定義するための枠組みも用意されており、決められたIFに沿って独自のProviderを実装すれば前述したパラメータストア以外にも任意のパラメータストアからパラメータ値が取得可能です。

このブログではミニマムな独自Providerを実装し、Google CloudのSecretManagerからシークレット値を取得してみます。

環境

今回利用した環境です

  • Node.js: v20.11.1
  • @aws-lambda-powertools/parameters: 2.7.0
  • @google-cloud/secret-manager: 5.6.0

やってみる

独自Providerの実装

コード全体は以下のようになりました

import { BaseProvider } from "@aws-lambda-powertools/parameters/base";
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
import type * as gax from "google-gax";
import type { ClientOptions } from "google-gax";

class GcloudSecretManagerServiceClient extends SecretManagerServiceClient {
  // SecretManagerServiceClientをそのまま使うと
  // `Failed to add user agent middleware Error: The client provided does not match the expected interface`
  // のwarnログが出力される
  // warnログ抑制のため期待されるinterfaceのダミー実装を組み込む

  public middlewareStack: object;
  public config: object;

  constructor(
    opts?: ClientOptions,
    gaxInstance?: typeof gax | typeof gax.fallback,
  ) {
    super(opts, gaxInstance);
    this.config = {};
    this.middlewareStack = {
      identify: () => [],
      addRelativeTo: () => {},
    };
  }

  send(): void {}
}

export class GcloudSecretManagerProvider extends BaseProvider {
  public declare client: GcloudSecretManagerServiceClient;

public constructor() {
    super({
      proto:
        GcloudSecretManagerServiceClient as new () => GcloudSecretManagerServiceClient,
    });
  }

  protected async _get(name: string): Promise<string> {
    const [res] = await this.client.accessSecretVersion({ name });

    return res.payload?.data?.toString() ?? "";
  }

  protected async _getMultiple(
    _path: string,
    _options?: unknown,
  ): Promise<Record<string, unknown> | undefined> {
    throw new Error("Method not implemented.");
  }
}

ポイントを解説していきます。

まずSecretManagerServiceClientを継承したGcloudSecretManagerServiceClientというクラスを定義しています

class GcloudSecretManagerServiceClient extends SecretManagerServiceClient {
  // SecretManagerServiceClientをそのまま使うと
  // `Failed to add user agent middleware Error: The client provided does not match the expected interface`
  // のwarnログが出力される
  // warnログ抑制のため期待されるinterfaceのダミー実装を組み込む

  public middlewareStack: object;
  public config: object;

  constructor(
    opts?: ClientOptions,
    gaxInstance?: typeof gax | typeof gax.fallback,
  ) {
    super(opts, gaxInstance);
    this.config = {};
    this.middlewareStack = {
      identify: () => [],
      addRelativeTo: () => {},
    };
  }

  send(): void {}
}

このクラスを定義するのは必須では無いですが、独自Providerの内部で利用するクラスがconfigmiddlewareStackというフィールドを持っていないとFailed to add user agent middleware Error: The client provided does not match the expected interfaceというwarnログが出力されてうっとおしいので、ダミーのフィールドを定義しています。

ちなみにwarnログを出力している処理はこのあたりですisSdkClientのチェックがfalseの場合にwarnログが出力されます。

https://github.com/aws-powertools/powertools-lambda-typescript/blob/23a33dc52db9d2bb8cfa8ff043abcc1020ff958e/packages/commons/src/awsSdkUtils.ts#L87-L105

isSdkClientの実装はこちらです。このチェック処理に合わせてダミーのフィールドを定義してやればwarnログの出力が回避できます。

https://github.com/aws-powertools/powertools-lambda-typescript/blob/23a33dc52db9d2bb8cfa8ff043abcc1020ff958e/packages/commons/src/awsSdkUtils.ts#L17-L33

メインとなる独自Providerの実装です

export class GcloudSecretManagerProvider extends BaseProvider {
  public declare client: GcloudSecretManagerServiceClient;

public constructor() {
    super({
      proto:
        GcloudSecretManagerServiceClient as new () => GcloudSecretManagerServiceClient,
    });
  }

  public async get(name: string): Promise<Record<string, unknown> | undefined> {
    return super.get(name) as Promise<Record<string, unknown> | undefined>;
  }

  public async getMultiple(path: string, _options?: unknown): Promise<void> {
    await super.getMultiple(path);
  }

  protected async _get(name: string): Promise<string> {
    const [res] = await this.client.accessSecretVersion({ name });

    return res.payload?.data?.toString() ?? "";
  }

  protected async _getMultiple(
    _path: string,
    _options?: unknown,
  ): Promise<Record<string, unknown> | undefined> {
    throw new Error("Method not implemented.");
  }
}

まずconstructorですが、ここで基底クラスのコンストラクタを呼び出してGcloudSecretManagerServiceClientのインスタンスを生成します。protoの型がnew (config?: unknown) => unknownなので、as new () => GcloudSecretManagerServiceClientを付けてGcloudSecretManagerServiceClientのコンストラクタによってGcloudSecretManagerServiceClientが生成されることを明示します。

続いて_get に基底クラスの_getをオーバーライドして独自のパラメータ値取得処理を実装します。コンストラクタでthis.clientGcloudSecretManagerServiceClientのインスタンスがセットされているので、this.client.accessSecretVersion({ name })でGoogle CloudのSecret Managerからシークレット値を取得して値を返却します。

_getMultipleに関しては今回はちゃんと実装しないので、Errorをthrowするだけの簡易な実装を入れて型エラーを回避しています。

独自Providerを使ってみる

独自Providerの準備ができたので実際に使ってみます。以下のコードで独自Providerを使ったシークレット値が取得可能です。

import { GcloudSecretManagerProvider } from "./gcloud-secret-manager-provider";

const main = async () => {
  const secretName =
    "projects/<プロジェクトNo>/secrets/<シークレット名>/versions/latest";

  const secretsProvider = new GcloudSecretManagerProvider();

  const secret = await secretsProvider.get(secretName);
  console.log(`secret: ${secret}`);
};

main();

実際に動かしてみると...

$npx ts-node index.ts
secret: <Google Cloudから取得したシークレット値>

無事にシークレット値が出力されました!!

まとめ

AWS Lambda Powertools for TypeScriptのParameters utilityで独自のProviderを実装するサンプルをご紹介しました。今回はミニマムな実装としていますが、基底クラスであるBaseProviderは継承先で各種処理を拡張することで様々なオプションを処理できるように設計されています。デフォルトでは非対応のパラメータストアから値を取得しつつ、Powertoolsの便利機能の恩恵を受けたい場合は独自Providerの実装も検討してみてください。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.